/**
 * Copyright (c) 2024 fduncanh
 * All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 */

// it should only start and stop the media_data_store that handles all HLS transactions, without
// otherwise participating in them.  

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <assert.h>

#include "raop.h"
#include "airplay_video.h"

struct media_item_s {
  char *uri;
  char *playlist;
  int num;
  int count;
  float duration;
};

struct airplay_video_s {
    raop_t *raop;
    char apple_session_id[37];
    char playback_uuid[37];
    char *uri_prefix;
    char *language_name;
    char *language_code;
    const char *lang;
    char local_uri_prefix[23];
    int next_uri;
    int FCUP_RequestID;
    float start_position_seconds;
    float resume_position_seconds;
    playback_info_t *playback_info;
    // The local port of the airplay server on the AirPlay server
    unsigned short airplay_port;
    char *master_playlist;
    media_item_t *media_data_store;
    int num_uri;
};

//  initialize airplay_video service.
airplay_video_t *airplay_video_init(raop_t *raop, unsigned short http_port,
                               const char *lang, const char *session_id) {
    char uri[] = "http://localhost:xxxxx";
    assert(raop);

    /* calloc guarantees that the 36-character strings apple_session_id and 
       playback_uuid are null-terminated */
    airplay_video_t *airplay_video =  (airplay_video_t *) calloc(1, sizeof(airplay_video_t));

    if (!airplay_video) {
        return NULL;
    }

    airplay_video->lang = lang;
     /* create local_uri_prefix string */
    strncpy(airplay_video->local_uri_prefix, uri, sizeof(airplay_video->local_uri_prefix));
    char *ptr  = strstr(airplay_video->local_uri_prefix, "xxxxx");
    snprintf(ptr, 6, "%-5u", http_port);
    ptr = strstr(airplay_video->local_uri_prefix, " ");
    if (ptr) {
        *ptr = '\0';
    }

    //printf(" %p %p\n", airplay_video, get_airplay_video(raop));

    airplay_video->raop = raop;
    airplay_video->FCUP_RequestID = 0;

    size_t len = strlen(session_id);
    assert(len == 36);
    strncpy(airplay_video->apple_session_id, session_id, len);
        
    airplay_video->start_position_seconds = 0.0f;

    airplay_video->uri_prefix = NULL;
    airplay_video->language_code = NULL;
    airplay_video->language_name = NULL;
    
    airplay_video->media_data_store = NULL;
    airplay_video->master_playlist = NULL;
    airplay_video->num_uri = 0;
    airplay_video->next_uri = 0;
    return airplay_video;
}

// destroy the airplay_video service
void
airplay_video_destroy(airplay_video_t *airplay_video)
{
    if (airplay_video->uri_prefix) {
        free(airplay_video->uri_prefix);
    }
    if (airplay_video->language_name) {
        free(airplay_video->language_name);
    }
    if (airplay_video->language_code) {
       free(airplay_video->language_code);
    }
    if (airplay_video->media_data_store) {
        destroy_media_data_store(airplay_video);
    }
    if (airplay_video->master_playlist){
        free (airplay_video->master_playlist);
    }
    free (airplay_video);
}

const char *get_apple_session_id(airplay_video_t *airplay_video) {
    return airplay_video->apple_session_id;
}

float get_start_position_seconds(airplay_video_t *airplay_video) {
    return airplay_video->start_position_seconds;
}

float get_resume_position_seconds(airplay_video_t *airplay_video) {
    return airplay_video->resume_position_seconds;
}

void set_start_position_seconds(airplay_video_t *airplay_video, float start_position_seconds) {
    airplay_video->start_position_seconds = start_position_seconds;
}

void set_resume_position_seconds(airplay_video_t *airplay_video, float resume_position_seconds) {
    airplay_video->resume_position_seconds = resume_position_seconds;
}

void set_playback_uuid(airplay_video_t *airplay_video, const char *playback_uuid) {
    size_t len = strlen(playback_uuid);
    assert(len == 36);
    memcpy(airplay_video->playback_uuid, playback_uuid, len);
    (airplay_video->playback_uuid)[len] = '\0';
}

const char *get_playback_uuid(airplay_video_t *airplay_video) {
    return (const char *) airplay_video->playback_uuid; 
}

void set_uri_prefix(airplay_video_t *airplay_video, char *uri_prefix) {
    if (airplay_video->uri_prefix) {
        free (airplay_video->uri_prefix);
    }
    airplay_video->uri_prefix = uri_prefix;
}

const char *get_uri_prefix(airplay_video_t *airplay_video) {
  return (const char *) airplay_video->uri_prefix;
}

void set_language_name(airplay_video_t *airplay_video, char *language_name) {
    if (airplay_video->language_name) {
        free (airplay_video->language_name);
    }
    airplay_video->language_name = language_name;
}

const char *get_language_name(airplay_video_t *airplay_video) {
  return (const char *)airplay_video->language_name;
}

void set_language_code(airplay_video_t *airplay_video, char *language_code) {;
    if (airplay_video->language_code) {
        free (airplay_video->language_code);
    }
    airplay_video->language_code = language_code;
}

const char *get_language_code(airplay_video_t *airplay_video) {
  return (const char *) airplay_video->language_code;
}

char *get_uri_local_prefix(airplay_video_t *airplay_video) {
    return airplay_video->local_uri_prefix;
}

int get_next_FCUP_RequestID(airplay_video_t *airplay_video) {    
    return ++(airplay_video->FCUP_RequestID);
}

void  set_next_media_uri_id(airplay_video_t *airplay_video, int num) {
    airplay_video->next_uri = num;
}

int get_next_media_uri_id(airplay_video_t *airplay_video) {
    return airplay_video->next_uri;
}

void store_master_playlist(airplay_video_t *airplay_video, char *master_playlist) {
    if (airplay_video->master_playlist) {
        free (airplay_video->master_playlist);
    }
    airplay_video->master_playlist = master_playlist;
}

typedef struct language_s {
    const char *start;
    int len;
    bool is_default;
    char code[6];
    char *name;
} language_t;

language_t* master_playlist_process_language(const char * data, int *slices, int *language_count) {
    *language_count = 0;
    const char *ptr = data;
    int count = 0, count1 = 0;
    while (ptr) {
        ptr = strstr(ptr,"#EXT-X-MEDIA:URI=");
        if(!ptr) {
            break;
        }
        ptr = strstr(ptr, "LANGUAGE=");
        if(!ptr) {
            break;
        }
        ptr = strstr(ptr,"YT-EXT-AUDIO-CONTENT-ID=");
        if(!ptr) {
            break;
        }
        count++;
    }
    if (count == 0) {
        return NULL;
    }
    language_t *languages = (language_t *) calloc(count + 2, sizeof(language_t));
    size_t length = 0;
    ptr = data;
    for (int i = 1; i <= count; i++) {
        char *end;
	int len_name;
        if (!(ptr = strstr(ptr, "#EXT-X-MEDIA"))) {
            break;
        }
        if (i == 1) {
            length = (int) (ptr - data);
            languages[0].start = data;
            languages[0].len = length;
            *languages[0].code = '\0';
            languages[0].name = NULL;
        }
        languages[i].start = ptr;

	if (!(ptr = strstr(ptr, "DEFAULT="))) {
            break;
        }
	ptr += strlen("DEFAULT=");
	languages[i].is_default = !strncmp(ptr, "YES", strlen("YES"));
	if (!(ptr = strstr(ptr, "NAME="))) {
            break;
        }
	ptr += strlen("NAME=");
	end = strchr(++ptr,'"');
	if (!end) {
            break;
        }
	len_name = end - ptr;
	languages[i].name = (char *) calloc(len_name + 1, sizeof(char));
	memcpy(languages[i].name, ptr, len_name *sizeof(char));
	if (!(ptr = strstr(ptr, "LANGUAGE="))) {
            break;
        }
        if (!(ptr = strchr(ptr,'"'))) {
            break;
        }
        if (!(end = strchr(++ptr,'"'))) {
            break;
        }
        strncpy(languages[i].code, ptr, end - ptr);
        if (!(ptr = strchr(ptr,'\n'))) {
            break;
        }
        count1++;
        languages[i].len = (int) (ptr + 1 - languages[i].start);
	length += languages[i].len;
    }
    assert (count1 == count);
    
    languages[count + 1].start = ++ptr;
    languages[count + 1].len = strlen(ptr);
    *languages[count + 1].code = '\0';
    languages[count + 1].name = NULL;

    length += languages[count + 1].len;
    assert(length == strlen(data));
    *slices = count + 2;

    int copies = 0;
    for (int i = 1; i < count; i++) {
        if (!strcmp(languages[i].code, languages[1].code)) {
            copies++;
        }
     }

    *language_count = count/copies;
    assert(count == *language_count * copies);

    /* verify expected structure of language choice information */
    for (int i = 1; i <= count; i++) {
  	int j = i - *language_count;
        if (j > 0) {
            assert (!strcmp(languages[i].code, languages[j].code));
        }
    }
    return languages;
}

char * select_master_playlist_language(airplay_video_t *airplay_video, char *master_playlist) {
    int language_count, slices;  
    language_t *languages;
    assert(master_playlist);
    if (!(languages = master_playlist_process_language(master_playlist,
                                                       &slices, &language_count))) {
        return master_playlist;
    }

    /* audio is offered in multiple languages */ 

    char *code = NULL;
    char *name = NULL;

    assert(airplay_video);
    printf("%d available languages:\n\n", language_count);
    int i_default = -1;
    
    const char *language_name = get_language_name(airplay_video);
    for (int i = 1; i <= language_count; i ++) {
        if (language_name) {
            if (!strcmp(language_name, languages[i].name)) {
                i_default = i;
            }
        } else if (languages[i].is_default) {
            i_default = i;
        }
        printf("%2d %-5.5s \"%s\" %s\n",i, languages[i].code, languages[i].name, (languages[i].is_default ? "(DEFAULT)" : ""));
    }
    printf("\n");
    assert(i_default >= 0);

    const char *ptrc = airplay_video->lang;;
    code = NULL;
    name = NULL;
    while (ptrc){
        for (int i = 1; i <= language_count; i++) {
            if (!strncmp(languages[i].code, ptrc, 2)) {
                code = languages[i].code;
                name = languages[i].name;
                printf("language choice: %s \"%s\" (based on prefered languages list %s)\n\n",
                       code, name,  airplay_video->lang);
                break;
            }
        }
        if (code) {
            break;
        }
        ptrc = strchr(ptrc,':');
        if(ptrc) {
            ptrc++;
            if (strlen(ptrc) < 2) {
                break;
            }
        }
    }

    if (!code) {
        code = languages[i_default].code;
        name = languages[i_default].name; 
        if (airplay_video->lang) {
            printf("no match with prefered language list %s\n", airplay_video->lang);
        }
        if (language_name) {
            printf("using HLS-specified language choice: %s \"%s\"\n\n", code, name); 
        } else {
            printf("using default language choice: %s \"%s\"\n\n", code, name);
        }
    } 

    /* update stored language code, name if changed */
    if (name != language_name) {   /* compare addresses */
        int len = strlen(name);
        char *new_language_name = (char *) calloc(len + 1, sizeof(char));
        memcpy(new_language_name, name, len);
        set_language_name(airplay_video, new_language_name);
        len = strlen(code);
        char *new_language_code = (char *) calloc(len + 1, sizeof(char));
        memcpy(new_language_code, code, len);
        set_language_code(airplay_video, new_language_code);
    }
    
    int len = 0;
    for (int i = 0; i < slices; i++) {
        if (strlen(languages[i].code) == 0 || !strcmp(languages[i].code, code)) {
            len += languages[i].len;	
        }
    }
    char *new_master_playlist = (char *) calloc(len + 1, sizeof(char));

    char *ptr = new_master_playlist;
    for (int i = 0; i < slices; i++) {
        if (strlen(languages[i].code) == 0 || !strcmp(languages[i].code, code)) {
            strncpy(ptr, languages[i].start, languages[i].len);
            ptr += languages[i].len;
        }
    }

    for (int i = 1; i <= slices - 2 ; i++) {
        free (languages[i].name);
    }
    free (languages);
    free (master_playlist);
    
    return new_master_playlist;
}

char *get_master_playlist(airplay_video_t *airplay_video) {
    return  airplay_video->master_playlist;
}

/* media_data_store */

int get_num_media_uri(airplay_video_t *airplay_video) {
    return airplay_video->num_uri;
}

void destroy_media_data_store(airplay_video_t *airplay_video) {
    media_item_t *media_data_store = airplay_video->media_data_store; 
    if (media_data_store) {
        for (int i = 0; i < airplay_video->num_uri ; i ++ ) {
            if (media_data_store[i].uri) {
                free (media_data_store[i].uri);
            }
            if (media_data_store[i].playlist) {
                free (media_data_store[i].playlist);
            }
        }
    }
    free (media_data_store);
    airplay_video->num_uri = 0;
}

void create_media_data_store(airplay_video_t * airplay_video, char ** uri_list, int num_uri) {  
    destroy_media_data_store(airplay_video);
    media_item_t *media_data_store = calloc(num_uri, sizeof(media_item_t));
    for (int i = 0; i < num_uri; i++) {
        media_data_store[i].uri = uri_list[i];
        media_data_store[i].playlist = NULL;
        media_data_store[i].num = i;
    }
    airplay_video->media_data_store = media_data_store;
    airplay_video->num_uri = num_uri;
}

int store_media_playlist(airplay_video_t *airplay_video, char * media_playlist, int *count, float *duration, int num) {
    media_item_t *media_data_store = airplay_video->media_data_store;
    if ( num < 0 ||  num >= airplay_video->num_uri) {
        return -1;
    } else if (media_data_store[num].playlist) {
        return -2;
    }
    /* dont store duplicate media paylists */
    for (int i = 0; i < num ; i++) {
        if (strcmp(media_data_store[i].uri, media_data_store[num].uri) == 0) {
            assert(strcmp(media_data_store[i].playlist, media_playlist) == 0);
            media_data_store[num].num = i;
            free (media_playlist);
            return 1;
        }
    }
    media_data_store[num].playlist = media_playlist;
    media_data_store[num].count = *count;
    media_data_store[num].duration = *duration;
    return 0;
}

char * get_media_playlist(airplay_video_t *airplay_video, int *count, float *duration, const char *uri) {
    media_item_t *media_data_store = airplay_video->media_data_store;
    if (media_data_store == NULL) {
        return NULL;
    }
    for (int i = 0; i < airplay_video->num_uri; i++) {
        if (strstr(media_data_store[i].uri, uri)) {
            *count = media_data_store[media_data_store[i].num].count;
            *duration = media_data_store[media_data_store[i].num].duration;
            return media_data_store[media_data_store[i].num].playlist;
        }
    }
    return NULL;
}

char * get_media_uri_by_num(airplay_video_t *airplay_video, int num) {
    media_item_t * media_data_store = airplay_video->media_data_store;
    if (num >= 0 && num < airplay_video->num_uri) {
        return  media_data_store[num].uri;
    }
    return NULL;
}

int analyze_media_playlist(char *playlist, float *duration) {
    float next;
    int count = 0;
    char *ptr = strstr(playlist, "#EXTINF:");
    *duration = 0.0f;
    while (ptr != NULL) {
        char *end;
        ptr += strlen("#EXTINF:");
        next = strtof(ptr, &end);
        *duration += next;
        count++;
        ptr = strstr(end, "#EXTINF:");
    }
    return count;
}

/* parse Master Playlist, make table of Media Playlist uri's that it lists */
int create_media_uri_table(const char *url_prefix, const char *master_playlist_data,
                           int datalen, char ***media_uri_table, int *num_uri) {
    char *ptr = strstr(master_playlist_data, url_prefix);
    char ** table = NULL;
    if (ptr == NULL) {
        return -1;
    }
    int count = 0;
    while (ptr != NULL) {
        char *end = strstr(ptr, "m3u8");
        if (end == NULL) {
            return 1;
        }
        end += sizeof("m3u8");
        count++;
        ptr = strstr(end, url_prefix);
    }
    table  = (char **)  calloc(count, sizeof(char *));
    if (!table) {
      return -1;
    }
    for (int i = 0; i < count; i++) {
        table[i] = NULL;
    }
    ptr = strstr(master_playlist_data, url_prefix);
    count = 0;
    while (ptr != NULL) {
        char *end = strstr(ptr, "m3u8");
	char *uri;
        if (end == NULL) {
            return 0;
        }
        end += sizeof("m3u8");
        size_t len = end - ptr - 1;
	uri  = (char *) calloc(len + 1, sizeof(char));
	memcpy(uri , ptr, len);
        table[count] = uri;
        uri =  NULL;	
	count ++;
	ptr = strstr(end, url_prefix);
    }
    *num_uri = count;

    *media_uri_table = table;
    return 0;
}

/* Adjust uri prefixes in the Master Playlist, for sending to the Media Player */
char *adjust_master_playlist (char *fcup_response_data, int fcup_response_datalen,
                              const char *uri_prefix, char *uri_local_prefix) {
    size_t uri_prefix_len = strlen(uri_prefix);
    size_t uri_local_prefix_len = strlen(uri_local_prefix);
    int counter = 0;
    char *ptr = strstr(fcup_response_data, uri_prefix);
    while (ptr != NULL) {
        counter++;
        ptr++;
        ptr = strstr(ptr, uri_prefix);
    }

    size_t len = uri_local_prefix_len - uri_prefix_len;
    len *= counter;
    len += fcup_response_datalen;
    int byte_count = 0;
    int new_len = (int) len;
    char *new_master = (char *) malloc(new_len + 1);
    new_master[new_len] = '\0';
    char *first = fcup_response_data;
    char *new = new_master;
    char *last = strstr(first, uri_prefix);
    counter  = 0;
    while (last != NULL) {
        counter++;
        len = last - first;
        memcpy(new, first, len);
        byte_count += len;
        first = last + uri_prefix_len;
        new += len;
        memcpy(new, uri_local_prefix, uri_local_prefix_len);
        byte_count += uri_local_prefix_len;
        new += uri_local_prefix_len;
        last = strstr(last + uri_prefix_len, uri_prefix);
        if (last  == NULL) {
            len = fcup_response_data  + fcup_response_datalen  - first;
            memcpy(new, first, len);
            byte_count += len;
            break;
        }
    }
    assert(byte_count == new_len); 
    return new_master;
}

char *adjust_yt_condensed_playlist(const char *media_playlist) {
/* this copies a Media Playlist into a null-terminated string. 
   If it has the "#YT-EXT-CONDENSED-URI" header, it is also expanded into 
   the full Media Playlist format.
   It  returns a pointer to the expanded playlist, WHICH MUST BE FREED AFTER USE */

    const char *base_uri_begin;
    const char *params_begin;
    const char *prefix_begin;
    size_t base_uri_len;
    size_t params_len;
    size_t prefix_len;
    const char* ptr = strstr(media_playlist, "#EXTM3U\n");

    ptr += strlen("#EXTM3U\n");
    assert(ptr);
    if (strncmp(ptr, "#YT-EXT-CONDENSED-URL", strlen("#YT-EXT-CONDENSED-URL"))) {
        size_t len = strlen(media_playlist);
        char * playlist_copy = (char *) malloc(len + 1);
        memcpy(playlist_copy, media_playlist, len);
        playlist_copy[len] = '\0';
        return playlist_copy;
    }
    ptr = strstr(ptr, "BASE-URI=");
    base_uri_begin = strchr(ptr, '"');
    base_uri_begin++;
    ptr = strchr(base_uri_begin, '"');
    base_uri_len = ptr - base_uri_begin;
    char *base_uri = (char *) calloc(base_uri_len + 1, sizeof(char));
    assert(base_uri);
    memcpy(base_uri, base_uri_begin, base_uri_len);  //must free

    ptr = strstr(ptr, "PARAMS=");
    params_begin = strchr(ptr, '"');
    params_begin++;
    ptr = strchr(params_begin,'"');
    params_len = ptr - params_begin;
    char *params = (char *) calloc(params_len + 1, sizeof(char));
    assert(params);
    memcpy(params, params_begin, params_len);  //must free

    ptr = strstr(ptr, "PREFIX=");
    prefix_begin = strchr(ptr, '"');
    prefix_begin++;
    ptr = strchr(prefix_begin,'"');
    prefix_len = ptr - prefix_begin;
    char *prefix = (char *) calloc(prefix_len + 1, sizeof(char));
    assert(prefix);
    memcpy(prefix, prefix_begin, prefix_len);  //must free

    /* expand params */
    int nparams = 0;
    int *params_size = NULL;
    const char **params_start = NULL;
    if (strlen(params)) {
        nparams = 1;
        char * comma = strchr(params, ',');
        while (comma) {
            nparams++;
            comma++;
            comma = strchr(comma, ',');
        }
        params_start = (const char **) calloc(nparams, sizeof(char *));  //must free
        params_size = (int *)  calloc(nparams, sizeof(int));     //must free
        ptr = params;
        for (int i = 0; i < nparams; i++) {
            comma = strchr(ptr, ',');
            params_start[i] = ptr;
            if (comma) {
                params_size[i] = (int) (comma - ptr);
                ptr = comma;
                ptr++;
            } else {
                params_size[i] = (int) (params + params_len - ptr);
                break;
            }
        }
    }

    int count = 0;
    ptr = strstr(media_playlist, "#EXTINF");
    while (ptr) {
        count++;
        ptr = strstr(++ptr, "#EXTINF");
    }

    size_t old_size = strlen(media_playlist);
    size_t new_len = old_size;
    new_len += count * (base_uri_len + params_len);

    int byte_count = 0;
    char * new_playlist = (char *) malloc(new_len + 1);
    new_playlist[new_len] = '\0';
    const char *old_pos = media_playlist;
    char *new_pos = new_playlist;
    ptr = old_pos;
    ptr = strstr(old_pos, "#EXTINF:");
    size_t len = ptr - old_pos;
    /* copy header section before chunks */
    memcpy(new_pos, old_pos, len);
    byte_count += len;
    old_pos += len;
    new_pos += len;
    while (ptr) {
        /* for each chunk */
        const char *end = NULL;
        char *start = strstr(ptr, prefix);
        len = start - ptr;
        /* copy first line of chunk entry */
        memcpy(new_pos, old_pos, len);
        byte_count += len;
        old_pos += len;
        new_pos += len;
	
	/* copy base uri  to replace prefix*/
        memcpy(new_pos, base_uri, base_uri_len);
        byte_count += base_uri_len;
        new_pos += base_uri_len;
        old_pos += prefix_len;
        ptr = strstr(old_pos, "#EXTINF:");

        /* insert the PARAMS separators on the slices line  */
        end = old_pos;
        int last = nparams - 1;
        for (int i = 0; i < nparams; i++) {
            if (i != last) {
                end = strchr(end, '/');
            } else {
                /* the next line starts with either #EXTINF (usually) 
                or #EXT-X-ENDLIST (at last chunk)*/
	        end = strstr(end, "#EXT");
            }
            *new_pos = '/';
            byte_count++;
            new_pos++;
            memcpy(new_pos, params_start[i], params_size[i]);
            byte_count += params_size[i];
            new_pos += params_size[i];
            *new_pos = '/';
            byte_count++;
            new_pos++;

            len = end - old_pos;
            end++;

            memcpy (new_pos, old_pos, len);
            byte_count += len;
            new_pos += len;
            old_pos += len;
            if (i != last) {
                old_pos++; /* last entry is not followed by "/" separator */
            }
        }
    }
    /* copy tail */
     
    len = media_playlist + strlen(media_playlist) - old_pos;
    memcpy(new_pos, old_pos, len);
    byte_count += len;
    new_pos += len;
    old_pos += len;

    assert(byte_count == new_len);

    free (prefix);
    free (base_uri);
    free (params);
    if (params_size) {
        free (params_size);
    }
    if (params_start) {
        free (params_start);
    }  

    return new_playlist;
}
